﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Legend.Threading
{
    /// <summary>
    /// A class that allows for scheduling of method calls.
    /// </summary>
    public class Scheduler
        : IScheduler
    {
        

        private ActionPriorityQueue actionQueue;
        private ISynchronizationManager synchronizationManager;
        private SystemTimeProvider systemTime;
        private ITimer timer;
        private TimerFactory timerFactory;

        

        

        /// <summary>
        /// Creates a new instance.
        /// </summary>
        /// <param name="systemTime">A delegate that returns the current system time.</param>
        /// <param name="timerFactory">A factory used to create ITimer objects.</param>
        /// <param name="synchronizationManager">An object used to manage the synchronization of the
        /// threaded calls.</param>
        public Scheduler(TimerFactory timerFactory, ISynchronizationManager synchronizationManager,
                         SystemTimeProvider systemTime)
        {
            Require.ThatArgument(timerFactory).Named("timerFactory").IsNotNull();
            Require.ThatArgument(systemTime).Named("systemTime").IsNotNull();
            Require.ThatArgument(synchronizationManager).Named("synchronizationManager").IsNotNull();

            this.timerFactory = timerFactory;
            this.systemTime = systemTime;
            actionQueue = new ActionPriorityQueue();
            this.synchronizationManager = synchronizationManager;
        }

        ~Scheduler()
        {
            Dispose(false);
        }

        

        

        /// <summary>
        /// Schedules the specified action to be invoked at the time specified.
        /// </summary>
        /// <param name="callback">The action to be invoked.</param>
        /// <param name="callbackTime">The time when to invoke the action.</param>
        /// <remarks>The action will be invoked no earlier than the time specified
        /// but it can not be guaranteed to be invoked at the time specified.</remarks>
        public void Schedule(Action callback, DateTime callbackTime)
        {
            Require.ThatArgument(callback).Named("callback").IsNotNull();

            using (synchronizationManager.AcquireWriteLock())
            {
                if (actionQueue.IsEmpty || actionQueue.NextDueCall.Key > callbackTime)
                {
                    ReplaceCurrentTimer(callbackTime);
                }

                actionQueue.Add(callbackTime, callback);
            }
        }

        /// <summary>
        /// Removes the first occurance of the spcified action scheduled at the 
        /// specified time.
        /// </summary>
        /// <param name="callback">The callback to remove.</param>
        /// <param name="callbackTime">The time the callback is scheduled
        /// for.</param>
        public void Remove(Action callback, DateTime callbackTime)
        {
            Require.ThatArgument(callback).Named("callback").IsNotNull();

            using (synchronizationManager.AcquireWriteLock())
            {
                if (callbackTime.Equals(actionQueue.NextDueCall.Key) && actionQueue.NextDueCall.Value.Count == 1)
                {
                    actionQueue.Remove(callbackTime, callback);
                    SetTimerForNextDueCall();
                }
                else
                {
                    actionQueue.Remove(callbackTime, callback);
                }
            }
        }

        /// <summary>
        /// Disposes the Scheduler, freeing all resources in use.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void ReplaceCurrentTimer(DateTime callbackTime)
        {
            EnsureThatOldTimerIsRemoved();
            TimeSpan waitTime = GetWaitTime(callbackTime);
            RegisterNewTimer(waitTime);
        }

        private void EnsureThatOldTimerIsRemoved()
        {
            if (timer != null)
            {
                timer.Elapsed -= timer_Elapsed;
                timer.Dispose();
                timer = null;
            }
        }

        private void RegisterNewTimer(TimeSpan waitTime)
        {
            timer = timerFactory(waitTime);
            timer.Elapsed += timer_Elapsed;
            timer.Start();
        }

        private TimeSpan GetWaitTime(DateTime callbackTime)
        {
            TimeSpan waitTime = callbackTime.Subtract(systemTime());
            waitTime = waitTime < TimeSpan.FromTicks(1) ? TimeSpan.FromTicks(1) : waitTime;

            return waitTime;
        }

        private void timer_Elapsed(object sender, EventArgs e)
        {
            using (synchronizationManager.AcquireWriteLock())
            {
                OnTimerElapsed((ITimer) sender);
            }
        }

        private void OnTimerElapsed(ITimer timer)
        {
            if (!Equals(timer, this.timer))
            {
                return;
            }

            InvokeAndRemoveDueCalls();
            SetTimerForNextDueCall();
        }

        private void SetTimerForNextDueCall()
        {
            if (!actionQueue.IsEmpty)
            {
                ReplaceCurrentTimer(actionQueue.NextDueCall.Key);
            }
            else
            {
                EnsureThatOldTimerIsRemoved();
            }
        }

        private void InvokeAndRemoveDueCalls()
        {
            KeyValuePair<DateTime, List<Action>> calls = actionQueue.NextDueCall;

            foreach (Action call in calls.Value)
            {
                call();
            }

            actionQueue.RemoveAll(calls.Key);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (timer != null)
                {
                    timer.Dispose();
                }

                synchronizationManager.Dispose();
            }
        }

        private class ActionPriorityQueue
        {
            private SortedList<DateTime, List<Action>> scheduledActions;

            public ActionPriorityQueue()
            {
                scheduledActions = new SortedList<DateTime, List<Action>>();
            }

            public List<Action> this[DateTime callbackTime]
            {
                get { return scheduledActions[callbackTime]; }
            }

            public KeyValuePair<DateTime, List<Action>> NextDueCall
            {
                get { return scheduledActions.FirstOrDefault(); }
            }

            public bool IsEmpty
            {
                get { return scheduledActions.Count == 0; }
            }

            public void Add(DateTime callbackTime, Action callback)
            {
                List<Action> actions = null;

                if (!scheduledActions.TryGetValue(callbackTime, out actions))
                {
                    actions = new List<Action>();
                    scheduledActions.Add(callbackTime, actions);
                }

                actions.Add(callback);
            }

            public void Remove(DateTime callbackTime, Action callback)
            {
                List<Action> actions = null;

                if (scheduledActions.TryGetValue(callbackTime, out actions))
                {
                    if (actions.Count > 1)
                    {
                        actions.Remove(callback);
                    }
                    else
                    {
                        scheduledActions.Remove(callbackTime);
                    }
                }
            }

            public void RemoveAll(DateTime callbackTime)
            {
                scheduledActions.Remove(callbackTime);
            }
        }

        
    }
}